package com.google.code.sms.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.LongRange;

/**
 * Miscellaneous string utility methods.
 *
 * @author Sindre Mehus
 */
public final class StringUtil {

    public static final String ENCODING_LATIN = "ISO-8859-1";
    public static final String ENCODING_UTF8 = "UTF-8";
    private static final DateFormat ISO_8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    private static final String[][] HTML_SUBSTITUTIONS = {
        {"&", "&amp;"},
        {"<", "&lt;"},
        {">", "&gt;"},
        {"'", "&#39;"},
        {"\"", "&#34;"},};
    private static final String[][] MIME_TYPES = {
        {"mp3", "audio/mpeg"},
        {"ogg", "audio/ogg"},
        {"oga", "audio/ogg"},
        {"ogx", "application/ogg"},
        {"aac", "audio/mp4"},
        {"m4a", "audio/mp4"},
        {"flac", "audio/flac"},
        {"wav", "audio/x-wav"},
        {"wma", "audio/x-ms-wma"},
        {"ape", "audio/x-monkeys-audio"},
        {"mpc", "audio/x-musepack"},
        {"shn", "audio/x-shn"},
        {"flv", "video/x-flv"},
        {"avi", "video/avi"},
        {"mpg", "video/mpeg"},
        {"mpeg", "video/mpeg"},
        {"mp4", "video/mp4"},
        {"m4v", "video/x-m4v"},
        {"mkv", "video/x-matroska"},
        {"mov", "video/quicktime"},
        {"wmv", "video/x-ms-wmv"},
        {"ogv", "video/ogg"},
        {"divx", "video/divx"},
        {"m2ts", "video/MP2T"},
        {"ts", "video/MP2T"},
        {"gif", "image/gif"},
        {"jpg", "image/jpeg"},
        {"jpeg", "image/jpeg"},
        {"png", "image/png"},
        {"bmp", "image/bmp"},
        {"vtt", "text/vtt"},
        {"srt", "text/plain"},};
        
    private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "|"};

    /**
     * Disallow external instantiation.
     */
    private StringUtil() {
    }

    /**
     * Returns the specified string converted to a format suitable for HTML. All
     * single-quote, double-quote, greater-than, less-than and ampersand
     * characters are replaces with their corresponding HTML Character Entity
     * code.
     *
     * @param s the string to convert
     * @return the converted string
     */
    public static String toHtml(String s) {
        if (s == null) {
            return null;
        }
        for (String[] substitution : HTML_SUBSTITUTIONS) {
            if (s.contains(substitution[0])) {
                s = s.replaceAll(substitution[0], substitution[1]);
            }
        }
        return s;
    }

    /**
     * Formats the given date to a ISO-8601 date/time format, and UTC timezone.
     * <p/>
     * The returned date uses the following format: 2007-12-17T14:57:17
     *
     * @param date The date to format
     * @return The corresponding ISO-8601 formatted string.
     */
    public static String toISO8601(Date date) {
        if (date == null) {
            return null;
        }

        synchronized (ISO_8601_DATE_FORMAT) {
            return ISO_8601_DATE_FORMAT.format(date);
        }
    }

    /**
     * Removes the suffix (the substring after the last dot) of the given
     * string. The dot is also removed.
     *
     * @param s The string in question, e.g., "foo.mp3".
     * @return The string without the suffix, e.g., "foo".
     */
    public static String removeSuffix(String s) {
        int index = s.lastIndexOf('.');
        return index == -1 ? s : s.substring(0, index);
    }

    /**
     * Returns the proper MIME type for the given suffix.
     *
     * @param suffix The suffix, e.g., "mp3" or ".mp3".
     * @return The corresponding MIME type, e.g., "audio/mpeg". If no MIME type
     * is found, <code>application/octet-stream</code> is returned.
     */
    public static String getMimeType(String suffix) {
        for (String[] map : MIME_TYPES) {
            if (map[0].equalsIgnoreCase(suffix) || ('.' + map[0]).equalsIgnoreCase(suffix)) {
                return map[1];
            }
        }
        return "application/octet-stream";
    }

    /**
     * Converts a byte-count to a formatted string suitable for display to the
     * user. For instance:
     * <ul>
     * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
     * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
     * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
     * </ul>
     * This method assumes that 1 KB is 1024 bytes.
     *
     * @param byteCount The number of bytes.
     * @param locale The locale used for formatting.
     * @return The formatted string.
     */
    public static synchronized String formatBytes(long byteCount, Locale locale) {

        // More than 1 GB?
        if (byteCount >= 1024 * 1024 * 1024) {
            NumberFormat gigaByteFormat = new DecimalFormat("0.00 GB", new DecimalFormatSymbols(locale));
            return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024));
        }

        // More than 1 MB?
        if (byteCount >= 1024 * 1024) {
            NumberFormat megaByteFormat = new DecimalFormat("0.0 MB", new DecimalFormatSymbols(locale));
            return megaByteFormat.format((double) byteCount / (1024 * 1024));
        }

        // More than 1 KB?
        if (byteCount >= 1024) {
            NumberFormat kiloByteFormat = new DecimalFormat("0 KB", new DecimalFormatSymbols(locale));
            return kiloByteFormat.format((double) byteCount / 1024);
        }

        return byteCount + " B";
    }

    /**
     * Formats a duration with minutes and seconds, e.g., "93:45"
     */
    public static String formatDuration(int secs) {
        int hours = (secs / 3600) % 24;
        int minutes = (secs / 60) % 60;
        int seconds = secs % 60;

        StringBuilder string = new StringBuilder();
        
        if (hours > 0) {
            string.append(hours).append(":");
        }
        
        if (minutes < 10) {
            string.append("0");
        }
        string.append(minutes).append(":");
        
        if (seconds < 10) {
            string.append("0");
        }
        string.append(seconds);
        
        return string.toString();
    }

    /**
     * Splits the input string. White space is interpreted as separator token.
     * Double quotes are interpreted as grouping operator. <br/>
     * For instance, the input
     * <code>"u2 rem "greatest hits""</code> will return an array with three
     * elements:
     * <code>{"u2", "rem", "greatest hits"}</code>
     *
     * @param input The input string.
     * @return Array of elements.
     */
    public static String[] split(String input) {
        if (input == null) {
            return new String[0];
        }

        Pattern pattern = Pattern.compile("\".*?\"|\\S+");
        Matcher matcher = pattern.matcher(input);

        List<String> result = new ArrayList<String>();
        while (matcher.find()) {
            String element = matcher.group();
            if (element.startsWith("\"") && element.endsWith("\"") && element.length() > 1) {
                element = element.substring(1, element.length() - 1);
            }
            result.add(element);
        }

        return result.toArray(new String[result.size()]);
    }

    /**
     * Reads lines from the given input stream. All lines are trimmed. Empty
     * lines and lines starting with "#" are skipped. The input stream is always
     * closed by this method.
     *
     * @param in The input stream to read from.
     * @return Array of lines.
     * @throws IOException If an I/O error occurs.
     */
    public static String[] readLines(InputStream in) throws IOException {
        BufferedReader reader = null;

        try {
            reader = new BufferedReader(new InputStreamReader(in));
            List<String> result = new ArrayList<String>();
            for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                line = line.trim();
                if (!line.startsWith("#") && line.length() > 0) {
                    result.add(line);
                }
            }
            return result.toArray(new String[result.size()]);

        } finally {
            IOUtils.closeQuietly(in);
            IOUtils.closeQuietly(reader);
        }
    }

    /**
     * Converts the given string of whitespace-separated integers to an
     * <code>int</code> array.
     *
     * @param s String consisting of integers separated by whitespace.
     * @return The corresponding array of ints.
     * @throws NumberFormatException If string contains non-parseable text.
     */
    public static int[] parseInts(String s) {
        if (s == null) {
            return new int[0];
        }

        String[] strings = StringUtils.split(s);
        int[] ints = new int[strings.length];
        for (int i = 0; i < strings.length; i++) {
            ints[i] = Integer.parseInt(strings[i]);
        }
        return ints;
    }

    /**
     * Determines whether a is equal to b, taking null into account.
     *
     * @return Whether a and b are equal, or both null.
     */
    public static boolean isEqual(Object a, Object b) {
        return a == null ? b == null : a.equals(b);
    }

    /**
     * Parses a locale from the given string.
     *
     * @param s The locale string. Should be formatted as per the documentation
     * in {@link Locale#toString()}.
     * @return The locale.
     */
    public static Locale parseLocale(String s) {
        if (s == null) {
            return null;
        }

        String[] elements = s.split("_");

        if (elements.length == 0) {
            return new Locale(s, "", "");
        }
        if (elements.length == 1) {
            return new Locale(elements[0], "", "");
        }
        if (elements.length == 2) {
            return new Locale(elements[0], elements[1], "");
        }
        return new Locale(elements[0], elements[1], elements[2]);
    }

    /**
     * URL-encodes the input value using UTF-8.
     */
    public static String urlEncode(String s) {
        try {
            return URLEncoder.encode(s, StringUtil.ENCODING_UTF8);
        } catch (UnsupportedEncodingException x) {
            throw new RuntimeException(x);
        }
    }

    /**
     * Encodes the given string by using the hexadecimal representation of its
     * UTF-8 bytes.
     *
     * @param s The string to encode.
     * @return The encoded string.
     */
    public static String utf8HexEncode(String s) {
        if (s == null) {
            return null;
        }
        byte[] utf8;
        try {
            utf8 = s.getBytes(ENCODING_UTF8);
        } catch (UnsupportedEncodingException x) {
            throw new RuntimeException(x);
        }
        return String.valueOf(Hex.encodeHex(utf8));
    }

    /**
     * Decodes the given string by using the hexadecimal representation of its
     * UTF-8 bytes.
     *
     * @param s The string to decode.
     * @return The decoded string.
     * @throws Exception If an error occurs.
     */
    public static String utf8HexDecode(String s) throws Exception {
        if (s == null) {
            return null;
        }
        return new String(Hex.decodeHex(s.toCharArray()), ENCODING_UTF8);
    }

    /**
     * Calculates the MD5 digest and returns the value as a 32 character hex
     * string.
     *
     * @param s Data to digest.
     * @return MD5 digest as a hex string.
     */
    public static String md5Hex(String s) {
        if (s == null) {
            return null;
        }

        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            return new String(Hex.encodeHex(md5.digest(s.getBytes(ENCODING_UTF8))));
        } catch (Exception x) {
            throw new RuntimeException(x.getMessage(), x);
        }
    }

    /**
     * Returns the file part of an URL. For instance:
     * <p/>
     * <
     * code>
     * getUrlFile("http://archive.ncsa.uiuc.edu:80/SDG/Software/Mosaic/Demo/url-primer.html")
     * </code>
     * <p/>
     * will return "url-primer.html".
     *
     * @param url The URL in question.
     * @return The file part, or <code>null</code> if no file can be resolved.
     */
    public static String getUrlFile(String url) {
        try {
            String path = new URL(url).getPath();
            if (StringUtils.isBlank(path) || path.endsWith("/")) {
                return null;
            }

            File file = new File(path);
            String filename = file.getName();
            if (StringUtils.isBlank(filename)) {
                return null;
            }
            return filename;

        } catch (MalformedURLException x) {
            return null;
        }
    }

    /**
     * Rewrites the URL by changing the protocol, host and port.
     *
     * @param urlToRewrite The URL to rewrite.
     * @param urlWithProtocolHostAndPort Use protocol, host and port from this
     * URL.
     * @return The rewritten URL, or an unchanged URL if either argument is not
     * a proper URL.
     */
    public static String rewriteUrl(String urlToRewrite, String urlWithProtocolHostAndPort) {
        if (urlToRewrite == null) {
            return null;
        }

        try {
            URL urlA = new URL(urlToRewrite);
            URL urlB = new URL(urlWithProtocolHostAndPort);

            URL result = new URL(urlB.getProtocol(), urlB.getHost(), urlB.getPort(), urlA.getFile());
            return result.toExternalForm();
        } catch (MalformedURLException x) {
            return urlToRewrite;
        }
    }

    /**
     * Makes a given filename safe by replacing special characters like slashes
     * ("/" and "\") with dashes ("-").
     *
     * @param filename The filename in question.
     * @return The filename with special characters replaced by underscores.
     */
    public static String fileSystemSafe(String filename) {
        for (String s : FILE_SYSTEM_UNSAFE) {
            filename = filename.replace(s, "-");
        }
        return filename;
    }

    /**
     * Parses the given string as a HTTP header byte range. See chapter 14.36.1
     * in RFC 2068 for details.
     * <p/>
     * Only a subset of the allowed syntaxes are supported. Only ranges which
     * specify first-byte-pos are supported. The last-byte-pos is optional.
     *
     * @param range The range from the HTTP header, for instance "bytes=0-499"
     * or "bytes=500-"
     * @return A range object (using inclusive values). If the last-byte-pos is
     * not given, the end of the returned range is {@link Long#MAX_VALUE}. The
     * method returns <code>null</code> if the syntax of the given range is not
     * supported.
     */
    public static LongRange parseRange(String range) {
        if (range == null) {
            return null;
        }

        Pattern pattern = Pattern.compile("bytes=(\\d+)-(\\d*)");
        Matcher matcher = pattern.matcher(range);

        if (matcher.matches()) {
            String firstString = matcher.group(1);
            String lastString = StringUtils.trimToNull(matcher.group(2));

            long first = Long.parseLong(firstString);
            long last = lastString == null ? Long.MAX_VALUE : Long.parseLong(lastString);

            if (first > last) {
                return null;
            }

            return new LongRange(first, last);
        }
        return null;
    }

    public static String removeMarkup(String s) {
        if (s == null) {
            return null;
        }
        return s.replaceAll("<.*?>", "");
    }
}